iOS 编写高质量Objective-C代码(四)—— 协议与分类

《编写高质量OC代码》已顺利完成一二三四五六七八篇!
附上链接:
iOS 编写高质量Objective-C代码(一)—— 简介
iOS 编写高质量Objective-C代码(二)—— 面向对象
iOS 编写高质量Objective-C代码(三)—— 接口和API设计
iOS 编写高质量Objective-C代码(四)—— 协议与分类
iOS 编写高质量Objective-C代码(五)—— 内存管理机制
iOS 编写高质量Objective-C代码(六)—— block专栏
iOS 编写高质量Objective-C代码(七)—— GCD专栏
iOS 编写高质量Objective-C代码(八)—— 系统框架


本篇的主题是:协议与分类(protocol & category

先简单介绍一下今天的主角:协议分类

  • 协议(protocol):OC中的协议与Java里的接口(interface)类似,OC不支持多继承。但是可以通过协议来实现委托模式
  • 分类(category):分类可以为既有类添加新的功能。分类是把“双刃剑”,用得好可以发挥OC的高动态性;用的不好,会留下很多坑。所以,通过这篇文章让我们一起研究OC的一项语言特性:category

一、通过委托与数据源协议进行对象间通信

委托模式(又称代理):某对象将一类方法(任务)交给另一个对象帮忙完成。
类似于:老板把一类任务交给某个leader去完成。(当然多类任务就会对应多个leader去完成)。

举例来说,当某对象要从另一个对象获取数据时,就可以使用委托模式。通过实现数据源协议来获取数据,这种做法被称为“数据源协议”(Data Source Protocol)。类似于UITableViewUITableViewDataSource

再举例来说,当一个对象要有一些事件响应时,就可以使用委托模式。通过实现一个协议(一般称为delegate),让代理对象帮助该对象处理事件响应。类似于UITableViewUITableViewDelegate

请看图解:
  • 好处:通过协议来降低代码的耦合性。(解耦
    必要的时候协议还可以替代继承。因为遵守同一个协议的类可以有很多,不一定要继承。

百说不如一Demo:这是小编整理的关于Button动画的例子

  • QiCircleAnimationView.h:
@class QiAnimationButton;
@protocol QiAnimationButtonDelegate <NSObject>

@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;

@end


@interface QiAnimationButton : UIButton

@property (nonatomic, weak) id<QiAnimationButtonDelegate> delegate;

- (void)startAnimation;//!< 开始动画
- (void)stopAnimation;//!< 结束动画
- (void)reverseAnimation;//!< 最后的修改动画

@end
  • QiAnimationButton.m中:
    就可以通过这样的方式回调
if ([self.delegate respondsToSelector:@selector(animationButton:willStartAnimationWithCircleView:)]) {
    [self.delegate animationButton:self willStartAnimationWithCircleView:_circleView];
}

/* .... */

if ([self.delegate respondsToSelector:@selector(animationButton:didStartAnimationWithCircleView:)]) {
    [self.delegate animationButton:self didStartAnimationWithCircleView:_circleView];
}

等等等...

所以,就会写出很多类似于这样格式的代码:

if ([self.delegate respondsToSelector:@selector(xxxFunction)]) {
    [self.delegate xxxFunction];
}

解释:因为该协议内的方法是@optional修饰的,所以遵守协议的Class可以选择性地实现协议里的方法。因此,代理对象在调用回调方法时,需要先检查一下Class有没有实现该协议里的方法?如果实现了,就回调;如果没有实现,就接着往下走。

考虑性能优化:

大家设想一下,这样一个场景:回调方法被频繁回调。也就是说,某回调方法被调用的频率很高。那么每调用一次回调方法都要去查一下Class有没有实现该回调方法。所以性能上会变差。

解决方案:实现一个含有位段的结构体,把委托对象能否响应某个协议方法的信息缓存起来,以优化程序执行效率。

百说不如一Demo,下面请看小编整理的Demo~

  1. 声明一个结构体DelegateFlags
@interface QiAnimationButton () {
    
    struct DelegateFlags {
        int doWillStartAnimation : 1;
        int doDidStartAnimation : 1;
        int doWillStopAnimation : 1;
        int doDidStopAnimation : 1;
        int doDidRevisedAnimation : 1;
    };
}
  1. 声明一个属性:
@property (nonatomic, assign) struct DelegateFlags delegateFlags;
  1. 重写delegateset方法:将是否实现该协议方法的信息缓存起来
- (void)setDelegate:(id<QiAnimationButtonDelegate>)delegate {
    
    _delegate = delegate;
    _delegateFlags.doWillStartAnimation = [delegate respondsToSelector:@selector(animationButton:willStartAnimationWithCircleView:)];
    _delegateFlags.doDidStartAnimation = [delegate respondsToSelector:@selector(animationButton:didStartAnimationWithCircleView:)];
    _delegateFlags.doWillStopAnimation = [delegate respondsToSelector:@selector(animationButton:willStopAnimationWithCircleView:)];
    _delegateFlags.doDidStopAnimation = [delegate respondsToSelector:@selector(animationButton:didStopAnimationWithCircleView:)];
    _delegateFlags.doDidRevisedAnimation = [delegate respondsToSelector:@selector(animationButton:didRevisedAnimationWithCircleView:)];
}
  1. 直接通过_delegateFlags缓存的值判断能否回调
if (_delegateFlags.doWillStartAnimation) {
   [self.delegate animationButton:self willStartAnimationWithCircleView:_circleView];
}

/* .... */

if (_delegateFlags.doDidStartAnimation) {
   [self.delegate animationButton:self didStartAnimationWithCircleView:_circleView];
}

二、把复杂类的实现代码分散到便于管理的数个分类之中

  • 使用分类机制,把一些很复杂的类“瘦身”,划分成各个易于管理的分类。
  • 把私有方法作为一个单独的分类,已隐藏实现细节。

好处:
1. 把复杂的类拆成小块,解耦。易于维护,易于管理。
2. 便于调试:遇到问题能快速定位是那个分类。

小编看法:视具体情况而定,拆分的同时,也会多出很多文件。如果一个类过于臃肿(比如有几千行代码),可以考虑给他瘦身,拆分成多个分类。

三、总是为第三方分类的名称加前缀

  • 分类机制最大的功能:就是为不能修改源码的既有类中添加新的功能。

这时候我们要:

  • 在分类类名前,加上专有前缀。
  • 在分类方法名前,加上专有前缀。

最大限度上避免重名可能带来的bug,而且这种bug很难排查。

原因在于:分类的方法会直接添加在类中,而分类是在运行期把方法加入主类。这时候,如果出现方法重名,后一个写入的分类方法会把前一个覆盖掉。多次覆盖的结果总以最后一个分类为准。所以我们要加前缀,尽量防止出现重名带来的bug。

四、勿在分类中声明属性

不要在分类中声明属性,但可以在类扩展(extension)中声明属性,这样属性就不会暴露在外面。

举个例子:(类扩展)

// QiShare.m
@interface QiShare ()
/* 属性可以声明在这里 */
@end


@implementation QiShare
/* ... */
@end
  1. 不能在分类中直接声明属性。如果声明了,编译时会报如下警告:
    Property 'name' requires method 'setName:' to be defined - use @dynamic or provide a method implementation in this category
    解释:分类无法合成相关的实例变量,需要开发者为该属性实现存取方法(get和set)。因为没有生成实例变量,set方法行不通。get方法可以返回固定值。或者使用@dynamic声明(即不会声明实例变量和存取方法)。
  1. 通过关联对象,为分类添加属性。(详情见第二篇 - 第5条)

所以,
1. 建议把属性都放在主类中。
2. 不到迫不得已,尽量不要在分类中通过关联对象添加属性。因为关联对象的内存管理问题上很容易出错,使用时需要重点提防。

五、使用“class-continuation分类”隐藏实现细节

这里的“class-continuation分类” 指的就是 类扩展(extension)。

我们可以把一些私有的属性声明在类扩展里,这样在导入.h文件时,看不到类扩展声明的属性。
目的:把公共接口中向外暴露的内容最小化,隐藏一些属性和实现细节。

这里补充一个小知识点:大家都知道Objective-C,但听说过Objective-C++吗?

Objective-C++是Objective-C和C++的混编,编译时会生成.mm文件。
这时候会遇到一个问题:只要导入含有C++的.h,都会编译成.mm文件。因为只有.mm文件才能同时编译OC和C++。
那么,OC怎么解决呢?用类扩展

举个例子:

#import "OCClass.h"
#import "CppClass.cpp"

@interface OCClass () {
    SomeCppClass *_cppClass;
}

@end

@implementation OCClass

/* ... */

@end

这样,.h文件中就没有C++代码了,如果只看头文件甚至都不知道底层有C++的代码。其实,我们的系统也是这样做的。比如WebKit、CoreAnimation等,很多底层代码都是通过C++写的。

小结:类扩展的应用场景
1. 向类中新增实例变量或属性
2. 在.h文件中把属性声明为“只读”,而类的内部又想修改此属性,可以在类扩展中重声明为“可读写”。
3. 私有方法的原型可以声明在类扩展里。
4. 如果不想让外部知道类中遵守了哪些协议,可以在类扩展中遵守协议。

六、通过协议提供匿名对象

  1. 可以通过协议提供匿名对象,例如:id<someProtocol> delegate。delegate对象的类型不限,只要能遵从这个协议的对象都可以。协议里规定了对象所需要实现的方法。
  2. 使用匿名对象来隐藏类型名称和类名。
  3. 对象只要实现协议里的方法即可(@optional修饰的可以选择性实现),其余的实现细节都被隐藏起来了。

最后,特别致谢:《Effective Objective-C 2.0》第四章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容